/******************************************************************************************
 * Copyright (c) 2006-2009 Alex Robson, Rob Simmons, Craig Israel, and Rachel Twyford
 *
 * Use of this software in any form requires that you have both 
 * read and agreed to the following terms:
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the "Software"), 
 * to deal in the Software without restriction, including without limitation 
 * the rights to use, copy, modify, merge, publish, distribute, sublicense, 
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions: 
 *
 * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright notice, 
 * this list of conditions and the following disclaimer in the 
 * documentation and/or other materials provided with the distribution.
 * Neither the name of Nvigorate nor the names of its contributors may 
 * be used to endorse or promote products derived from this software without 
 * specific prior written permission. 
 *
 * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
 * DISCLAIMED. IN NO EVENT SHALL ANY CONTRIBUTOR BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * ****************************************************************************************/

using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using System.Text;
using Nvigorate.Common;
using Nvigorate.Common.Reflection;
using Nvigorate.Logging;
using Nvigorate.Relational.State;
using PostSharp.Extensibility;
using PostSharp.Laos;

namespace Nvigorate.Aspect
{
    /// <summary>
    /// Applying this attribute causes the StateAspect to get added to all property setters
    /// so that dirty/state tracking can be accomplished seamlessly. It also automatically
    /// implements the ITrackState interface and a concrete default implementation with the
    /// class PerstenceState if ITrackState is not implemented on the target already.
    /// </summary>
	[MulticastAttributeUsage(MulticastTargets.Class)]
	public class StateTrackingAspect : CompoundAspect
	{
        public override void ProvideAspects(object element, LaosReflectionAspectCollection collection)
		{
			Type targetType = element as Type;

            AddIdentityAdvice(collection, targetType);
            AddDirtyCheckingAdvice(collection, targetType);
            AddStateTrackingAdvice(collection, targetType);
		}

        protected void AddDirtyCheckingAdvice(LaosReflectionAspectCollection collection, Type targetType)
        {
            Type persistence = typeof (ITrackState);
            List<string> forbidden = Reflector.GetProperties(persistence);
            forbidden.AddRange(Reflector.GetPropertiesOfInterface<ITrackState>(targetType).Select(p => p.Name));

            OnPropertySet advice = new OnPropertySet();

            foreach(PropertyInfo property in targetType.GetProperties())
            {
                MethodInfo setter = property.GetSetMethod();

                if (setter != null)
                {
                    if(!setter.IsStatic && !forbidden.Contains(setter.Name.Replace("set_","")))
                    {
                        //Writes the property and class names that the tracking aspect is being applied to to the console
                        //during build to make it simpler to determine which properties are being tracked.
                        Console.WriteLine("Applying State Tracking Aspect to property {0} on type {1}", property.Name, targetType.FullName);
                        collection.AddAspect(setter, advice);
                    }
                }
            }
        }

        protected void AddIdentityAdvice(LaosReflectionAspectCollection collection, Type targetType)
        {
            var aspect = new AddIdentityAspect();
            collection.AddAspect(targetType, aspect);
        }

        protected void AddStateTrackingAdvice(LaosReflectionAspectCollection collection, Type targetType)
        {
            var aspect = new AddIStateAspect();
            collection.AddAspect(targetType, aspect);
        }

        public StateTrackingAspect()
        {
        }

        [Serializable]
        internal class AddIStateAspect : CompositionAspect
        {
            public override Type GetPublicInterface(Type containerType)
            {
                return typeof (ITrackState);
            }
            
            public override object CreateImplementationObject(InstanceBoundLaosEventArgs eventArgs)
            {
                return new StateTracking();
            }

            public override CompositionAspectOptions GetOptions()
            {
                return
                    CompositionAspectOptions.GenerateImplementationAccessor |
                    CompositionAspectOptions.IgnoreIfAlreadyImplemented;
            }
        }

        [Serializable]
        internal class AddIdentityAspect : CompositionAspect
        {
            public override object CreateImplementationObject(InstanceBoundLaosEventArgs eventArgs)
            {
                return new InstanceIdentifier();
            }

            public override Type GetPublicInterface(Type containerType)
            {
                return typeof(IIdentifiable);
            }

            public override CompositionAspectOptions GetOptions()
            {
                return
                    CompositionAspectOptions.GenerateImplementationAccessor |
                    CompositionAspectOptions.IgnoreIfAlreadyImplemented;
            }


        }


		[Serializable]
		internal class OnPropertySet : OnMethodBoundaryAspect
		{
            public override void OnEntry(MethodExecutionEventArgs eventArgs)
			{
                //Gets the persistance properties. This should *never* be null
                ITrackState tracked = eventArgs.Instance as ITrackState;
                string propertyName = eventArgs.Method.Name.Replace("set_", "");

                //If the object is in the process of deserialization or by some
                //freak chance doesn't implement IPerstable, just set the value
                //and exit
                if (tracked == null)
                {
                    base.OnEntry(eventArgs);
                    return;
                }

                try
			    {
                    if (tracked.Deserializing)
                    {
                        base.OnEntry(eventArgs);
                        return;
                    }

                    //Get the original value                   
                    object original = Reflector.Read(eventArgs.Instance, propertyName);
                    tracked.RaisePropertyChanging(propertyName);

                    //If the original value is null, we still need to set the dirty flag
                    //and add the property as changed but we have to be careful NOT
                    //to do anything else against the null
                    if (original == null)
                    {
                        tracked.InstanceState.AddChangedValue(propertyName, null);
                    }
                    else if (!original.Equals(eventArgs.GetReadOnlyArgumentArray()[0]))
                    {
                        tracked.InstanceState.AddChangedValue(propertyName, original);
                    }
			    }
			    catch (Exception ex)
			    {
			        LogManager.LogError(ex, Severities.Moderate, "Persistence Aspect Error on {0} property {1}",
			                            eventArgs.Instance.GetType().FullName, eventArgs.Method.Name);
			        //throw;
			    }
                finally
			    {
                    base.OnEntry(eventArgs);
			    }
			    
			}
		}
	}
}
